package my;

import debug.ConfigGroup;
import debug.ConfigSettings;
import debug.Debug;
import debug.Debug.DEBUG_LEVELS;

import java.io.BufferedReader;

import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Date;
import java.util.Properties;
import java.util.Vector;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.PosixParser;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;


public class Main {
	public enum STATE { STARTUP, LOADCONFIG, CREATECONFIG, CONNECT, GRAB_TEN, GRAB_OLD, CHECK_NEW, EMAIL, PRUNE, ERROR, EXIT }; 
	static String CONFIG_FILENAME = "config";
	static String ErrMesg = ""; 
	static public class Info {
		public ConfigSettings settings = new ConfigSettings();
		public Vector<Game> topTen = new Vector<Game>();
		public Vector<Game> oldSpec = new Vector<Game>();
		public Vector<Game> newSpec = new Vector<Game>();
		public Statement st = null;
		boolean sendEmail = true;
	}
	public static void main(String[] args) {
		
		 STATE state = STATE.STARTUP;
		 Info info = new Info();
		//Load Config file
		try {
			while( true ) {
				switch ( state ) {
				case STARTUP:
					state = getOpts(args, info);
					break;
				case LOADCONFIG:
					Debug.Info("Loading Config...");
					Status( state = LoadConfig(CONFIG_FILENAME, info));
					break;
				case CREATECONFIG:
					Debug.Info("Creating Default Config...");
					Status( state = CreateConfig( info ) );
					break;
				case CONNECT:
					Debug.Info("Connecting to database...");
					Status( state = ConnectToDB(info));
					break;
				case GRAB_TEN:
					//Grab list of current top ten specials
					Debug.Info("Grabbing top ten...");
					Status( state = GrabTopTen(info));
					break;
				case GRAB_OLD:
					Debug.Info("Grabbing old games...");
					//Grab list of old specials
					Status( state = GrabOldSpecials(info));
					break;
				case CHECK_NEW:
					Debug.Info("Checking for new games...");
					//Check if any new specials
					Status( state = CheckForNewSpecials(info) );
					break;
				case EMAIL:
					Debug.Info("Emailing...");
					//Email list of new specials
					Status( state = EmailSpecials(info));
					break;
				case PRUNE:
					//Remove old specials
					Debug.Info("Pruning...");
					Status(state = PruneExpiredSpecials(info));
					break;
				case ERROR:
					Debug.Perror("Error");
					Debug.Fatal("Press Any Key To Continue...");
			        new java.util.Scanner(System.in).nextLine();
			        Debug.Close();
			        System.exit(-1);
					break;
				case EXIT:
					Debug.Info("Exiting...");
					 Debug.Close();
					Debug.Info("Goodbye."); 
					return;
				}
			}
		}
		catch( Exception e ) {
			e.printStackTrace();
		}
	}
	@SuppressWarnings("static-access")
	public static STATE getOpts(String args[], Info info) {
		Options opt = new Options();
		opt.addOption("w","warn",false,"displays all warn messages");
		opt.addOption("v","verbose",false,"displays all warn and info messages");
		opt.addOption("d","debug",false,"displays all debugging messages");
		opt.addOption("h","help",false,"displays usage");
		opt.addOption("n","no-email",false,"refrains from sending out emails.");
		opt.addOption("c","create-config-file",false,"creates a config file with default settings.");
		opt.addOption(  OptionBuilder.withLongOpt("config")
				.hasArg()
				.withArgName("FILE")
				.withDescription("use config file FILE as the runtim config")
				.create());
		opt.addOption( OptionBuilder.withLongOpt("log")
				.hasArg()
				.withArgName("FILE")
				.withDescription("Logs all debugging messages to file FILE")
				.create());
		CommandLineParser parse = new PosixParser();
		try {
			CommandLine cmd = parse.parse(opt, args);
			if( cmd.hasOption('w')) {
				Debug.SetDebugLevel(DEBUG_LEVELS.WARN);
			}
			if( cmd.hasOption("v")) {
				Debug.SetDebugLevel(DEBUG_LEVELS.INFO);
			}
			if( cmd.hasOption("d")) {
				Debug.SetDebugLevel(DEBUG_LEVELS.ALL);
			}
			if( cmd.hasOption("h")) {
				HelpFormatter formatter = new HelpFormatter();
				formatter.printHelp("java -jar SteamSpecials.jar [ -ne | no-email ] [ -v ] [ -vv ]\n [ -vvv ] [ -h | --help ] ", opt);
				return STATE.EXIT;
			}
			if( cmd.hasOption("n")) {
				info.sendEmail = false;
			}
			else {
				info.sendEmail = true;
			}
			if( cmd.hasOption("config")) {
				CONFIG_FILENAME = cmd.getOptionValue("config");
			}
			if( cmd.hasOption("log")) {
				Debug.SetDebugLog(cmd.getOptionValue("log"));
			}
			if( cmd.hasOption('c')) {
				return STATE.CREATECONFIG;
			}
		}
		catch ( Exception e ) {
			Debug.HandleException(e);
			return STATE.ERROR;
		}
		return STATE.LOADCONFIG;
	}
	public static void FirstTimeRun(ConfigSettings settings, Statement st ) throws Exception {
		Debug.Warn("First Time run: Generating tables...");
		st.execute("CREATE TABLE " + settings.get("SQL_TABLE") + " (id int auto_increment primary key not null, name varchar(256) not null, price double not null, ctime double not null)");
		Debug.Warn("Success...");
	}
	public static void SetupConfigSettings( Info info ) throws Exception {
		info.settings.addGroup( new ConfigGroup().WithGDescription("Locale Settings")
				.WithGName("Locale")
				.WithOption("LANGUAGE", "","en")
				.WithOption("COUNTRY","","US")
				.WithOption("ENCODING", "","UTF-8")
			)
			.addGroup( new ConfigGroup().WithGDescription("SQL Information")
				.WithOption("SQL_URL", "","jdbc:mysql://localhost:3306/test")
				.WithOption("SQL_PASS","", "password")
				.WithOption("SQL_USER", "","root")
				.WithOption("SQL_TABLE", "", "games_test")
			)
			.addGroup( new ConfigGroup().WithGDescription("Email Information")
				.WithOption("SMTP_USER", "", "admin@localhost.com")
				.WithOption("SMTP_TO", "", "user@localhost.com")
				.WithOption("SMTP_PASS", "", "password")
				.WithOption("SMTP_HOST", "", "smtp.gmail.com")
				.WithOption("SMTP_PORT",  "", "465")
				.WithOption("SMTP_AUTH", "", "true")
			)
			.addGroup( new ConfigGroup().WithGDescription("Steam Information")
				.WithOption("DISCOUNT_ID", "Discount ID Tab Div id", "tab_Discounts_items")
				.WithOption("DISCOUNT_ITEM", "", "tab_row")
				.WithOption("SEARCH_NAME", "the div id for the section containing the name", "tab_desc")
				.WithOption("NAME_TAG", "the tag surrounding the name in the SEARCH_NAME", "h4")
				.WithOption("TAB_PRICE", "", "tab_price")
				.WithOption("TAB_PRICE_REGEX", "", ".*<br />")
				.WithOption("IMAGE_URL", "", "tab_item_img")
				.WithOption("DISCOUNT", "", "discount_pct")
				.WithOption("EXPIRE_SEC", "", "604800")
				.WithOption("TOP_URL", "","http://store.steampowered.com/")
			)
			.addGroup( new ConfigGroup().WithGDescription("Metascore Info")
				.WithOption("SEARCH_RESULT_CLASS", "","search_result_row")
				.WithOption("SEARCH_RESULT_NAME_TAG", "","h4")
				.WithOption("SEARCH_METASCORE_CLASS", "","search_metascore")
			);	
	}
	public static STATE LoadConfig(String file, Info info) {
		try {
			FileInputStream fstream = new FileInputStream(file);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			
			SetupConfigSettings(info);
			String line;
			String tokens[];
			int count = 0;
			
			while( null != (line = br.readLine())) {
				if( line.length() > 0 && line.charAt(0) != '#' ) {
					if(  line.length() > 2) {
						tokens = line.split("=");
						if( tokens.length == 2 ) {
							info.settings.put(tokens[0], tokens[1]);
						}
						else {
							Debug.Warn("Error parsing config line %d...", count);
						}
					}
					else {
						Debug.Warn("Error parsing config line %d....", count);
					}
				}
				++count;
			}
			info.settings.Validate();
		}
		catch ( Exception e ) {
			Debug.HandleException(e);
			return STATE.ERROR;
		}
		return STATE.CONNECT;
	}
	public static STATE CreateConfig(Info info)  {
		try {
			SetupConfigSettings(info);
			info.settings.CreatDefaultFile("config.default");
		} catch ( Exception e) {
			Debug.HandleException(e);
			return STATE.ERROR;
		}
		return STATE.EXIT;
	}
	public static STATE ConnectToDB(Info info) {
		try {
			Class.forName("com.mysql.jdbc.Driver");
			Connection con = DriverManager.getConnection(info.settings.get("SQL_URL"),
					info.settings.get("SQL_USER"), info.settings.get("SQL_PASS"));
			info.st = con.createStatement();
			if( !info.st.executeQuery("show tables like '"+ info.settings.get("SQL_TABLE") +"'").first() ) {
				FirstTimeRun(info.settings, info.st);
			}
		} catch( Exception e ) {
			Debug.HandleException(e);
			return STATE.ERROR;
		}
		return STATE.GRAB_TEN;
		
	}
	public static void Status( STATE st) {
		if( st != STATE.ERROR ) {
			Debug.Info("Success.\n");
		}
		else {
			Debug.Info("Failed!!!\n");
		}
	}
	public static STATE GrabTopTen(Info info) {
		Document doc;
		Element top;
		Elements items;
		info.topTen = new Vector<Game>();
		try {
			if( (doc = Jsoup.connect(info.settings.get("TOP_URL")).get() ) == null ) {
				return STATE.ERROR;
			}
			if( ( top = doc.getElementById(info.settings.get("DISCOUNT_ID"))) == null ) {
				return STATE.ERROR;
			}
			if( (items = top.getElementsByClass(info.settings.get("DISCOUNT_ITEM"))).size() == 0 ) {
				return STATE.ERROR;
			}
			for( Element e : items ) {
				info.topTen.add(new Game(e,info.settings));
			}
		}
		catch( Exception e) {
			Debug.HandleException(e);
			return STATE.ERROR;
		}
		Debug.MoreInfo("\n");
		for( Game g : info.topTen) {
			Debug.MoreInfo("\t%s %s %f...\n", g.Name(), g.Discount_Pct(), g.Price());
		}
		return STATE.GRAB_OLD;
	}
	
	public static STATE GrabOldSpecials(Info info)  {
		ResultSet rs;
		info.oldSpec= new Vector<Game>();
		try { 
			rs = info.st.executeQuery("SELECT * FROM " + info.settings.get("SQL_TABLE"));
			if( !rs.first() ) {
				return STATE.CHECK_NEW;
			}
			do {
				info.oldSpec.add( new Game(rs.getString("name"), rs.getDouble("price"), rs.getLong("ctime")));
			} while( rs.next());
		} catch (Exception e) {
			Debug.HandleException(e);
			return STATE.ERROR;
		}
		Debug.MoreInfo("\n");
		for( Game g : info.oldSpec) {
			Debug.MoreInfo("\t%s %s %f...\n", g.Name(), g.Discount_Pct(), g.Price());
		}
		return STATE.CHECK_NEW;
	}
	
	public static STATE CheckForNewSpecials(Info info) {
		info.newSpec = new Vector<Game>();
		boolean matchFound;
		for(Game gNew : info.topTen) {
			matchFound = false;
			for( Game gOld : info.oldSpec ) {
				if ( gNew.Name().equals( gOld.Name() )) {
					if( gNew.Price() == gOld.Price()) {
						matchFound = true;
						break;
					}
					//Update the old price with the new
					try {
						info.st.executeUpdate("UPDATE " + info.settings.get("SQL_TABLE") 
								+ " SET ctime=" + Double.toString(new Date().getTime() + Double.valueOf(info.settings.get("EXPIRE_SEC")) * 1000)
								+", price="+ Double.toString(gNew.Price()) +" WHERE name=\""+ gNew.Name() +"\"");
					} catch (Exception e) {
						Debug.HandleException(e);
						return STATE.ERROR;
					}
					break;
				}
			}
			if( !matchFound ) {
				info.newSpec.add(gNew);
				try {
					info.st.execute("INSERT INTO " + info.settings.get("SQL_TABLE") +" (name, price, ctime) VALUES (\""+ 
						gNew.Name() + "\","+ gNew.Price() +","+ Double.toString(new Date().getTime() 
						+ Double.valueOf(info.settings.get("EXPIRE_SEC")) * 1000) +")");
				} catch (Exception e) {
					Debug.HandleException(e);
					return STATE.ERROR;
				}
			}
		}
		for( Game g : info.newSpec ) {
			try {
				if( ! g.GrabMetascore() ) {
					Debug.Warn("%s Missing metascore...", g.Name());
				}
			} catch (Exception e) {
				//Debug.HandleException(e);
				Debug.Warn("recovering from non-stritical error...");
			}
		}
		if( info.newSpec.size() > 0 ) {
			Debug.MoreInfo("\n");
		}
		for( Game g : info.newSpec) {
			Debug.MoreInfo("\t%s %s %f...\n", g.Name(), g.Discount_Pct(), g.Price());
		}
		return STATE.EMAIL;
	}
	
	public static STATE EmailSpecials(Info info) throws Exception {
		if( info.sendEmail) {
			if( !info.newSpec.isEmpty()) {
				Properties props = new Properties();
				String BodyMessage = "<html>\n<body>\nUpdate on Steam Watch:\n<p>\n<table>";
				
				
				
				props.put("mail.smtp.host", info.settings.get("SMTP_HOST"));
				props.put("mail.smtp.socketFactory.port", info.settings.get("SMTP_PORT"));
				props.put("mail.smtp.socketFactory.class",
						"javax.net.ssl.SSLSocketFactory");
				props.put("mail.smtp.auth", "true");
				props.put("mail.smtp.port",  info.settings.get("SMTP_PORT"));
		 
				final String user = info.settings.get("SMTP_USER");
				final String pass = info.settings.get("SMTP_PASS");
				Session session = Session.getDefaultInstance(props,
					new javax.mail.Authenticator() {
						protected PasswordAuthentication getPasswordAuthentication() {
							return new PasswordAuthentication(user,pass);
						}
					});
		 
				try {
		 
					Message message = new MimeMessage(session);
					message.setFrom(new InternetAddress((info.settings.get("SMTP_USER"))));
					message.setRecipients(Message.RecipientType.BCC,
							InternetAddress.parse(info.settings.get("SMTP_TO")));
					message.setSubject("Steam Watch Update");
					
					for( Game g : info.newSpec ) {
						BodyMessage +="<tr><td>" + "<a href='" + g.HrefURL() +"'>" 
								+ g.ImageURL() + "</a></td><td><a href='" + g.HrefURL() +"'>" +g.Name() 
								+ "</a></td><td>" + g.PriceLocale() + "</td><td>" + g.Discount_Pct() +"</td><td>"+ g.Metascore() +"</td></tr>\n"; 
					}
					BodyMessage += "</table>\n</body>\n</html>\n";
					message.setContent(BodyMessage, "text/html");
					Debug.MoreInfo("%s", BodyMessage);
		 
					Transport.send(message);
		 
				
		 
				} catch (MessagingException e) {
					Debug.HandleException(e);
					return STATE.ERROR;
				}
			}
			else {
				Debug.Warn("No new specials...");
			}
		}
		else {
			Debug.Info("Skipping...");
		}
		return STATE.PRUNE;
		
	}
	
	public static STATE PruneExpiredSpecials(Info info) throws Exception {
		info.st.execute("DELETE FROM " + info.settings.get("SQL_TABLE") + " WHERE ctime<" + new Date().getTime());
		return STATE.EXIT;
	}
}
